Локальная версия с подсветкой
editor.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Интерактивный HTML редактор</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Универсальный путь: работает и на компе, и на сервере FastAPI -->
<link href="static/prism.css" rel="stylesheet" id="prism-theme" />
<style>
html, body {
margin: 0; padding: 0; height: 100%; overflow: hidden;
font-family: Consolas, Monaco, 'Andale Mono', monospace;
background-color: #121212; color: #e0e0e0;
}
body { display: flex; flex-direction: column; height: 100%; }
#container { flex: 1; display: flex; width: 100vw; height: 100%; overflow: hidden; }
.editor-wrapper {
position: relative; width: 50%; height: 100%; overflow: hidden; background-color: #1e1e1e;
}
/* Сверхточное наложение слоёв */
#editor, #highlighting {
margin: 0; padding: 15px; border: none; width: 100%; height: 100%;
font-size: 15px;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
line-height: 1.5;
position: absolute; top: 0; left: 0; box-sizing: border-box;
white-space: pre; overflow: auto; tab-size: 2;
}
#editor {
color: transparent; background: transparent; caret-color: #00ff00;
z-index: 2; resize: none; outline: none;
}
#highlighting { z-index: 1; pointer-events: none; }
/* Сброс стилей Prism */
pre[class*="language-"] { margin: 0 !important; padding: 0 !important; background: transparent !important; }
code[class*="language-"] { text-shadow: none !important; padding: 0 !important; }
iframe#preview { width: 50%; border: none; background: white; height: 100%; }
#dragbar { width: 6px; cursor: col-resize; background-color: #333; user-select: none; z-index: 20; transition: background 0.2s; }
#dragbar:hover { background-color: #4da6ff; }
button.btn-icon {
position: absolute; top: 10px; width: 34px; height: 34px;
background: #2a2a2a; border: 1px solid #444; cursor: pointer;
border-radius: 6px; z-index: 30; color: #fff;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s; opacity: 0.9;
}
button.btn-icon:hover { opacity: 1; background: #333; border-color: #4da6ff; }
#btn-copy { right: 50px; }
#btn-theme { right: 10px; }
button.btn-icon svg { width: 18px; height: 18px; fill: currentColor; }
/* Светлая тема */
body.light { background-color: #f0f0f0; }
body.light .editor-wrapper { background: #ffffff; }
body.light #editor { caret-color: #000; }
body.light #dragbar { background-color: #ddd; }
@media (max-width: 768px) {
#container { flex-direction: column; }
.editor-wrapper, iframe#preview { width: 100% !important; }
#dragbar { cursor: row-resize; width: 100%; height: 6px; }
}
</style>
</head>
<body>
<div id="container">
<div class="editor-wrapper" id="editor-panel">
<textarea id="editor" spellcheck="false" placeholder="Вставьте ваш HTML код сюда..."></textarea>
<pre id="highlighting" aria-hidden="true"><code class="language-html" id="highlighting-content"></code></pre>
<button class="btn-icon" id="btn-copy" title="Скопировать всё">
<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14 c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
<button class="btn-icon" id="btn-theme" title="Сменить тему">
<svg viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 0112.21 3a7 7 0 108.79 9.79z"/></svg>
</button>
</div>
<div id="dragbar"></div>
<iframe id="preview" sandbox="allow-scripts allow-same-origin allow-modals"></iframe>
</div>
<!-- Универсальный путь к JS -->
<script src="static/prism.js"></script>
<script>
const editor = document.getElementById('editor');
const highlightingContent = document.getElementById('highlighting-content');
const highlighting = document.getElementById('highlighting');
const preview = document.getElementById('preview');
const btnCopy = document.getElementById('btn-copy');
const btnTheme = document.getElementById('btn-theme');
const dragbar = document.getElementById('dragbar');
const editorWrapper = document.getElementById('editor-panel');
function update() {
let content = editor.value;
let escaped = content.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
if (content[content.length - 1] === "\n") escaped += " ";
highlightingContent.innerHTML = escaped;
if (typeof Prism !== 'undefined') {
highlightingContent.className = 'language-html';
Prism.highlightElement(highlightingContent);
}
preview.srcdoc = content;
localStorage.setItem('editor_code_flexy', content);
}
function syncScroll() {
highlighting.scrollTop = editor.scrollTop;
highlighting.scrollLeft = editor.scrollLeft;
}
editor.addEventListener('scroll', syncScroll);
editor.addEventListener('input', () => {
update();
syncScroll();
});
editor.onkeydown = function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
this.value = this.value.substring(0, start) + " " + this.value.substring(this.selectionEnd);
this.selectionEnd = start + 2;
update();
}
};
window.addEventListener('load', () => {
const saved = localStorage.getItem('editor_code_flexy');
if (saved) { editor.value = saved; } else {
editor.value = "<!-- Локальная подсветка активна! -->\n<html>\n<style>\n h1 { color: #00ff00; }\n</style>\n<script>\n console.log('Flexy AI Editor Loaded');\n<\/script>\n<body>\n <h1>Всё работает!</h1>\n</body>\n</html>";
}
update();
syncScroll();
});
btnCopy.onclick = () => {
navigator.clipboard.writeText(editor.value);
const original = btnCopy.innerHTML;
btnCopy.innerHTML = '<span style="color:#4da6ff; font-size:12px">OK</span>';
setTimeout(() => btnCopy.innerHTML = original, 1000);
};
btnTheme.onclick = () => {
document.body.classList.toggle('light');
};
let isDragging = false;
dragbar.onmousedown = (e) => { isDragging = true; document.body.style.cursor = window.innerWidth > 768 ? 'col-resize' : 'row-resize'; };
document.onmouseup = () => { isDragging = false; document.body.style.cursor = 'default'; };
document.onmousemove = (e) => {
if (!isDragging) return;
if (window.innerWidth > 768) {
let x = e.clientX;
if (x > 100 && x < window.innerWidth - 100) editorWrapper.style.width = x + 'px';
} else {
let y = e.clientY;
if (y > 50 && y < window.innerHeight - 50) editorWrapper.style.height = y + 'px';
}
};
</script>
</body>
</html>
main.py :
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles # Добавь это
app = FastAPI()
# Подключаем папку static по пути /static
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
async def editor(request: Request):
return templates.TemplateResponse("editor.html", {"request": request})
Для подсветки синтаксиса
Cоздать в папке проекта папку static. положить туда prism.css и! prism.js
Файлы можно взять prismjs.com/download выбрать Minified version Languages: Markup + HTML + XML + SVG + MathML + SSML + Atom + RSS CSS, C-like, JavaScript Themes: Tomorrow Night
CDN Версия c подсветкой
editor.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Flexy Interactive Editor</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Стили Prism: тема Tomorrow Night -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" crossorigin="anonymous" />
<style>
html, body {
margin: 0; padding: 0; height: 100%; overflow: hidden;
font-family: 'Consolas', 'Monaco', 'Andale Mono', monospace;
background-color: #121212; color: #e0e0e0;
}
body { display: flex; flex-direction: column; height: 100%; }
#container { flex: 1; display: flex; width: 100vw; height: 100%; overflow: hidden; }
.editor-wrapper {
position: relative; width: 50%; height: 100%; overflow: hidden; background-color: #1e1e1e;
}
/* Сверхточное наложение слоёв ввода и подсветки */
#editor, #highlighting {
margin: 0; padding: 20px; border: none; width: 100%; height: 100%;
font-size: 16px;
font-family: 'Consolas', 'Monaco', 'Andale Mono', monospace;
line-height: 1.5;
position: absolute; top: 0; left: 0; box-sizing: border-box;
white-space: pre; overflow: auto; tab-size: 2;
word-break: break-all;
}
#editor {
color: transparent; background: transparent; caret-color: #00ff00;
z-index: 2; resize: none; outline: none;
}
#highlighting { z-index: 1; pointer-events: none; }
/* Фиксы для Prism: убираем лишние отступы и тени */
pre[class*="language-"] { margin: 0 !important; padding: 0 !important; background: transparent !important; }
code[class*="language-"] {
text-shadow: none !important;
padding: 0 !important;
font-family: inherit !important;
background: transparent !important;
}
iframe#preview { width: 50%; border: none; background: white; height: 100%; }
#dragbar { width: 6px; cursor: col-resize; background-color: #333; user-select: none; z-index: 20; transition: background 0.2s; }
#dragbar:hover { background-color: #4da6ff; }
button.btn-icon {
position: absolute; top: 10px; width: 34px; height: 34px;
background: #2a2a2a; border: 1px solid #444; cursor: pointer;
border-radius: 6px; z-index: 30; color: #fff;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s; opacity: 0.9;
}
button.btn-icon:hover { opacity: 1; background: #333; border-color: #4da6ff; }
#btn-copy { right: 50px; }
#btn-theme { right: 10px; }
button.btn-icon svg { width: 18px; height: 18px; fill: currentColor; }
/* Светлая тема */
body.light { background-color: #f0f0f0; }
body.light .editor-wrapper { background: #ffffff; }
body.light #editor { caret-color: #000; }
body.light #dragbar { background-color: #ddd; }
@media (max-width: 768px) {
#container { flex-direction: column; }
.editor-wrapper, iframe#preview { width: 100% !important; }
#dragbar { cursor: row-resize; width: 100%; height: 6px; }
}
</style>
</head>
<body>
<div id="container">
<div class="editor-wrapper" id="editor-panel">
<textarea id="editor" spellcheck="false" placeholder="Вставьте ваш HTML код сюда..."></textarea>
<pre id="highlighting" aria-hidden="true"><code class="language-html" id="highlighting-content"></code></pre>
<button class="btn-icon" id="btn-copy" title="Скопировать всё">
<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14 c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
<button class="btn-icon" id="btn-theme" title="Сменить тему">
<svg viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 0112.21 3a7 7 0 108.79 9.79z"/></svg>
</button>
</div>
<div id="dragbar"></div>
<iframe id="preview" sandbox="allow-scripts allow-same-origin allow-modals"></iframe>
</div>
<!-- Подключаем скрипты Prism через CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js" crossorigin="anonymous"></script>
<script>
const editor = document.getElementById('editor');
const highlightingContent = document.getElementById('highlighting-content');
const highlighting = document.getElementById('highlighting');
const preview = document.getElementById('preview');
const btnCopy = document.getElementById('btn-copy');
const btnTheme = document.getElementById('btn-theme');
const dragbar = document.getElementById('dragbar');
const editorWrapper = document.getElementById('editor-panel');
function update() {
let content = editor.value;
// Экранирование для корректного отображения в слое подсветки
let escaped = content.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
if (content[content.length - 1] === "\n") escaped += " ";
highlightingContent.innerHTML = escaped;
// Инициируем подсветку Prism
if (window.Prism) {
Prism.highlightElement(highlightingContent);
}
// Обновляем превью в iframe
preview.srcdoc = content;
localStorage.setItem('editor_code_flexy', content);
}
function syncScroll() {
highlighting.scrollTop = editor.scrollTop;
highlighting.scrollLeft = editor.scrollLeft;
}
editor.addEventListener('scroll', syncScroll);
editor.addEventListener('input', () => {
update();
syncScroll();
});
// Вставка Tab как 2 пробелов
editor.onkeydown = function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
this.value = this.value.substring(0, start) + " " + this.value.substring(this.selectionEnd);
this.selectionEnd = start + 2;
update();
syncScroll();
}
};
window.addEventListener('load', () => {
const saved = localStorage.getItem('editor_code_flexy');
if (saved) {
editor.value = saved;
} else {
editor.value = "<!-- Полная подсветка (HTML, CSS, JS) активна! -->\n<html>\n<head>\n <style>\n h1 { color: #4da6ff; transition: 0.3s; }\n h1:hover { transform: scale(1.1); }\n </style>\n</head>\n<body>\n <h1>Привет!</h1>\n <button onclick=\"sayHi()\">Нажми меня</button>\n\n <script>\n function sayHi() {\n alert('Flexy AI: Всё работает идеально!');\n }\n <\/script>\n</body>\n</html>";
}
update();
syncScroll();
});
btnCopy.onclick = () => {
navigator.clipboard.writeText(editor.value);
const original = btnCopy.innerHTML;
btnCopy.innerHTML = '<span style="color:#00ff00; font-size:12px; font-weight:bold;">OK</span>';
setTimeout(() => btnCopy.innerHTML = original, 1000);
};
btnTheme.onclick = () => {
document.body.classList.toggle('light');
};
// Логика изменения размеров панелей
let isDragging = false;
dragbar.onmousedown = (e) => { isDragging = true; document.body.style.cursor = window.innerWidth > 768 ? 'col-resize' : 'row-resize'; };
document.onmouseup = () => { isDragging = false; document.body.style.cursor = 'default'; };
document.onmousemove = (e) => {
if (!isDragging) return;
if (window.innerWidth > 768) {
let x = e.clientX;
if (x > 100 && x < window.innerWidth - 100) editorWrapper.style.width = x + 'px';
} else {
let y = e.clientY;
if (y > 50 && y < window.innerHeight - 50) editorWrapper.style.height = y + 'px';
}
};
</script>
</body>
</html>
main.py:
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
app = FastAPI()
# Указываем папку с шаблонами
templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
async def editor(request: Request):
return templates.TemplateResponse("editor.html", {"request": request})
Версия без подсветки
editor.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Интерактивный HTML редактор</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
font-family: monospace, monospace;
background-color: #121212;
color: #e0e0e0;
}
body {
display: flex;
flex-direction: column;
height: 100%;
}
#container {
flex: 1;
display: flex;
width: 100vw;
height: 100%;
overflow: hidden;
}
.editor-wrapper, #preview {
box-sizing: border-box;
overflow: auto;
height: 100%;
}
.editor-wrapper {
position: relative;
width: 50%;
display: flex;
flex-direction: column;
}
textarea#editor {
flex: 1;
width: 100%;
border: none;
background-color: #1e1e1e;
color: #e0e0e0;
font-size: 16px;
padding: 10px 44px 10px 10px;
font-family: monospace, monospace;
resize: none;
outline: none;
white-space: pre;
}
iframe#preview {
width: 50%;
border: none;
background: white;
}
#dragbar {
width: 5px;
cursor: col-resize;
background-color: #444;
user-select: none;
}
button.btn-icon {
position: absolute;
top: 8px;
width: 32px; height: 32px;
background: transparent;
border: none;
cursor: pointer;
filter: invert(0.65);
opacity: 0.7;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: opacity 0.25s ease, filter 0.25s ease;
z-index: 10;
color: inherit;
}
button.btn-icon:hover { opacity: 1; filter: invert(0.9); }
#btn-copy { right: 44px; }
#btn-theme { right: 6px; }
button.btn-icon svg { width: 20px; height:20px; fill: currentColor; }
body.light {
background-color: #f5f5f5; color: #222;
}
body.light textarea#editor {
background: #fff; color: #222;
}
body.light iframe#preview {
background: #fff;
}
@media (max-width: 768px) {
#container {
flex-direction: column !important;
width: 100vw;
height: 100%;
}
.editor-wrapper, #preview {
width: 100% !important;
}
#dragbar {
cursor: row-resize;
width: 100%;
height: 5px;
}
#btn-copy, #btn-theme {
top: 6px;
width: 28px;
height: 28px;
}
#btn-copy { right: 40px; }
#btn-theme { right: 6px; }
}
</style>
</head>
<body>
<div id="container">
<div class="editor-wrapper" role="tabpanel" id="editor-panel" aria-hidden="false">
<textarea id="editor" placeholder="Пиши HTML здесь..."></textarea>
<button class="btn-icon" id="btn-copy" title="Скопировать код" aria-label="Скопировать код" type="button" tabindex="0" aria-live="polite" aria-atomic="true" >
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14 c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
<button class="btn-icon" id="btn-theme" title="Переключить тему" aria-label="Переключить тему" type="button" tabindex="0">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21 12.79A9 9 0 0112.21 3a7 7 0 108.79 9.79z"/></svg>
</button>
</div>
<div id="dragbar"></div>
<iframe id="preview" sandbox="allow-scripts allow-same-origin allow-modals" role="tabpanel" aria-hidden="true"></iframe>
</div>
<script>
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
const btnCopy = document.getElementById('btn-copy');
const btnTheme = document.getElementById('btn-theme');
const body = document.body;
const container = document.getElementById('container');
const dragbar = document.getElementById('dragbar');
const editorWrapper = document.querySelector('.editor-wrapper');
function updatePreview() {
preview.srcdoc = editor.value;
}
function setSizes() {
if (window.innerWidth <= 768) {
const h = window.innerHeight;
container.style.height = h + 'px';
const halfHeight = Math.floor(h / 2);
editorWrapper.style.height = halfHeight + 'px';
preview.style.height = halfHeight + 'px';
preview.style.width = '100%';
editorWrapper.style.width = '100%';
container.style.flexDirection = 'column';
dragbar.style.width = '100%';
dragbar.style.height = '5px';
dragbar.style.cursor = 'row-resize';
// Запретить прокрутку body
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
} else {
container.style.height = '';
editorWrapper.style.height = '100%';
preview.style.height = '100%';
editorWrapper.style.width = '50%';
preview.style.width = '50%';
container.style.flexDirection = 'row';
dragbar.style.width = '5px';
dragbar.style.height = '100%';
dragbar.style.cursor = 'col-resize';
document.documentElement.style.overflow = '';
document.body.style.overflow = '';
}
}
window.addEventListener('load', () => {
const saved = localStorage.getItem('flexy_code');
if (saved !== null) {
editor.value = saved;
updatePreview();
}
setSizes();
});
window.addEventListener('resize', setSizes);
window.addEventListener('orientationchange', setSizes);
editor.addEventListener('input', () => {
localStorage.setItem('flexy_code', editor.value);
updatePreview();
});
btnCopy.addEventListener('click', () => {
const text = editor.value;
function showCopied() {
btnCopy.textContent = 'Скопировано \u2713';
setTimeout(() => {
btnCopy.innerHTML = `
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14 c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
}, 1500);
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => showCopied()).catch(() => fallbackCopy());
} else {
fallbackCopy();
}
function fallbackCopy() {
editor.select();
try {
let success = document.execCommand('copy');
if (success) showCopied();
else alert('Не удалось скопировать');
} catch {
alert('Не удалось скопировать');
}
window.getSelection().removeAllRanges();
}
});
btnTheme.addEventListener('click', () => {
body.classList.toggle('light');
});
// Drag resize
let isDragging = false;
dragbar.addEventListener('mousedown', () => {
isDragging = true;
document.body.style.userSelect = 'none';
});
document.addEventListener('mouseup', () => {
isDragging = false;
document.body.style.userSelect = '';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
if (window.innerWidth > 768) {
const rect = container.getBoundingClientRect();
let x = e.clientX - rect.left;
const min = 100;
const max = container.clientWidth - min;
if (x < min) x = min;
if (x > max) x = max;
editorWrapper.style.width = x + 'px';
preview.style.width = (container.clientWidth - x - dragbar.offsetWidth) + 'px';
editorWrapper.style.height = '100%';
preview.style.height = '100%';
} else {
const rect = container.getBoundingClientRect();
let y = e.clientY - rect.top;
const min = 50;
const max = container.clientHeight - min;
if (y < min) y = min;
if (y > max) y = max;
editorWrapper.style.height = y + 'px';
preview.style.height = (container.clientHeight - y - dragbar.offsetHeight) + 'px';
editorWrapper.style.width = '100%';
preview.style.width = '100%';
}
});
</script>
</body>
</html>
main.py:
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
async def editor(request: Request):
return templates.TemplateResponse("editor.html", {"request": request})
Служба myeditor.service:
[Unit]
Description=Uvicorn FastAPI Service myeditor
After=network.target
[Service]
User=root
WorkingDirectory=/root/my_html_editor
ExecStart=/root/my_html_editor/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8083
Restart=always
[Install]
WantedBy=multi-user.target
Служба myeditor.service с TLS сертификатом:
[Unit]
Description=Uvicorn FastAPI Service myeditor
After=network.target
[Service]
User=root
WorkingDirectory=/root/my_html_editor
ExecStart=/root/my_html_editor/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8083 --ssl-certfile /etc/letsencrypt/live/server.tonicman.ru/fullchain.pem --ssl-keyfile /etc/letsencrypt/live/server.tonicman.ru/privkey.pem
Restart=always
[Install]
WantedBy=multi-user.target
Если настроена маскировка (Fallback) для 3X-UI + Nginx:
[Unit]
Description=Uvicorn FastAPI Service myeditor
After=network.target
[Service]
User=root
WorkingDirectory=/root/my_html_editor
ExecStart=/root/my_html_editor/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8083
Restart=always
[Install]
WantedBy=multi-user.target